import base64
import json
import traceback
import zipfile
from io import BytesIO
from mimetypes import guess_type
from threading import Thread

from django.core.files.base import ContentFile
from django.core.mail import EmailMultiAlternatives
from django.db import transaction
from django.db.models import Q, F, Sum
from django.forms import model_to_dict
from django.shortcuts import get_object_or_404
from django.template.loader import get_template
from django.utils.dateparse import parse_date
from django_tenants.utils import schema_context
from rest_framework import status
from rest_framework.decorators import authentication_classes, api_view, permission_classes
from rest_framework.exceptions import ValidationError
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response
from rest_framework_simplejwt.authentication import JWTAuthentication
from datetime import datetime
from django.utils import timezone

from apps.billing.models import Invoice
from apps.booking.helpers import cancel_booking
from apps.booking.models import Booking, Appointment
from apps.booking.signals import kiosk_check_in
from apps.booking.validations import validate_update_booking_api, handle_appointment_on_booking_update
from apps.core.utils import date_time as datetime_helper

from apps.booking.filters import DateRangeFilter, date_range_filter_queryset
from apps.booking.types import BookingStatusChoices
from apps.core.locks import request_unlock
from apps.core.pagination import paginated_queryset_response
from apps.location.filters import BookingFilter, booking_filter_queryset
from apps.location.helpers import save_room_operating_hours, save_room_closed_dates, save_room_price, \
    save_room_authorization
from apps.location.models import Location, OperatingHourLocation, LocationClosedDate, LocationTierSetting, Lock, Room, \
    RoomOperatingHours, RoomClosedDate, RoomPrice, RoomAuthorization, AdminBookingDetails, ProviderBillings
from apps.location.types import RoomPermission
from apps.users.models import Provider
from apps.users.permissions import IsProvider, IsWelcomeBoardAccount
from apps.core.utils.gcs_operations import generate_assigned_url, delete_file

from django.conf import settings


# Create your views here.


def save_operating_hours(location, opertaing_hours):
    for hour in opertaing_hours:
        OperatingHourLocation.objects.create(location=location, **hour)


def save_closed_dates(location, closed_dates):
    for date in closed_dates:
        date['date'] = datetime.fromtimestamp(int(date['date']))
        LocationClosedDate.objects.create(location=location, **date)


def get_spaces(spaces_mapping,location, operating_hours):
    space_dicts = json.loads(spaces_mapping)

    spaces = []
    for space_dict in space_dicts:
        spaces.append(Room(name=space_dict['name'], comment='',location=location
        ))

    rooms = Room.objects.bulk_create(spaces)
    room_operating_hours = []
    for room in rooms:
        for hour in operating_hours:
            room_operating_hours.append(RoomOperatingHours(room=room, day_of_week=hour.day_of_week, start_time=hour.start_time,
                                                           close_time=hour.close_time, is_useable=hour.is_useable))

    RoomOperatingHours.objects.bulk_create(room_operating_hours)

    spaces_mapping = json.loads(location.room_map_mapping)
    for mapping in spaces_mapping:
        space = [space for space in rooms if space.name ==
                 mapping['name']][0]
        mapping['id'] = str(space.id)

    location.room_map_mapping = json.dumps(spaces_mapping)
    location.save()
    return spaces


def resolve_room_map_zip(room_map, location, operating_hours):
    try:
        floor_map_zip = zipfile.ZipFile(room_map)

        floor_map_image_bytes = floor_map_zip.read('floor-map/floormap.png')

        floor_map_image = ContentFile(floor_map_image_bytes, 'floormap.png')
        location.room_map = floor_map_image

        floor_map_mapping_bytes = floor_map_zip.read('floor-map/floormap.json')
        floor_map_mapping = floor_map_mapping_bytes.decode('utf8')
        location.room_map_mapping = floor_map_mapping

        location.save()

        return get_spaces(floor_map_mapping,location, operating_hours)
    except Exception as e:
        print(e)
        message = {
            'room_map': ["Zip file must contain 'floormap.png'"
                         " and 'floormap.json'"]
        }
        raise ValidationError(message)


def delete_bookings_for_room(room_id):
    """Deletes bookings associated with the given room_id across all providers."""

    providers = Provider.objects.all()
    for provider in providers:
        with schema_context(provider.schema_name):
            Booking.objects.filter(room=room_id).delete()


@api_view(('GET', 'POST'))
@authentication_classes([JWTAuthentication])
@permission_classes([IsAdminUser | IsProvider | IsWelcomeBoardAccount])
@transaction.atomic
def location_api(request):
    if request.method == 'POST':
        user = request.user
        if user.is_provider:
            return Response({"detail": "You do not have permission to perform this action"},
                            status=status.HTTP_403_FORBIDDEN)
        data = request.data
        operating_hours = None
        closed_dates = None
        if 'operating_hours' in data:
            operating_hours = data.pop('operating_hours')
        if 'closed_dates' in data:
            closed_dates = data.pop('closed_dates')


        location = Location.objects.create(**data)

        if operating_hours:
            save_operating_hours(location, operating_hours)
        if closed_dates:
            save_closed_dates(location, closed_dates)


        return Response({'id': location.id, 'updated_at': location.updated_at,
                         'created_at': location.created_at, 'name': location.name,
                         'image': generate_assigned_url(location.image.url) if location.image else None,
                         'contact_person': location.contact_person,
                         'email': location.email, 'address': location.address,
                         'phone_number': location.phone_number, 'room_map': location.room_map.url if location.room_map else None,
                         'room_map_mapping': location.room_map_mapping},
                        status=status.HTTP_201_CREATED)

    if request.method == 'GET':
        with schema_context("public"):
            ordering = request.GET.get('ordering', '-created_at')
            locations = Location.objects.all().order_by(ordering)
            ethera_locations = list()
            for get_location in locations:
                ethera_location = {
                    'total_rooms': get_location.room.all().count(),
                    'id': get_location.id, 'updated_at': get_location.updated_at,
                    'created_at': get_location.created_at, 'name': get_location.name,
                    'image': get_location.image.url if get_location.image else None,
                    'contact_person': get_location.contact_person,
                    'operating_hours': OperatingHourLocation.objects.filter(location=get_location).all().values(),
                    'email': get_location.email, 'address': get_location.address,
                    'phone_number': get_location.phone_number, 'room_map': get_location.room_map.url if get_location.room_map else None,
                    'room_map_mapping': get_location.room_map_mapping
                }
                ethera_locations.append(ethera_location)
            return paginated_queryset_response(queryset=ethera_locations, request=request)


@api_view(('POST',))
@authentication_classes([JWTAuthentication])
@permission_classes([IsAdminUser | IsProvider])
@transaction.atomic
def location_image_api(request, location_pk):
    try:
        get_location = Location.objects.filter(id=location_pk).first()
        if not get_location:
            return Response({
                'success': False,
                'message': f'Failed to upload image because location does not exist'
            }, status=status.HTTP_400_BAD_REQUEST)
        image = request.data.get('image', None)
        room_map = request.FILES.get('room_map', None)
        if room_map:
            operating_hours = OperatingHourLocation.objects.filter(location=get_location)
            resolve_room_map_zip(room_map,get_location, operating_hours)
        if image:

            get_image = request.FILES.getlist('image')[0]
            get_location.image = get_image
            get_location.save()
        return Response(
            {'id': get_location.id, 'updated_at': get_location.updated_at,
             'created_at': get_location.created_at, 'name': get_location.name,
             'image': get_location.image.url if get_location.image else None,
             'contact_person': get_location.contact_person,
             'operating_hours': OperatingHourLocation.objects.filter(location=get_location).all().values(),
             'email': get_location.email, 'address': get_location.address,
             'phone_number': get_location.phone_number, 'room_map': get_location.room_map.url if get_location.room_map else None,
             'room_map_mapping': get_location.room_map_mapping},
            status=status.HTTP_200_OK
        )
    except Exception as e:
        return (Response({
            'success': False,
            'message': f'Failed to upload image because of {str(e)}'
        }, status=status.HTTP_400_BAD_REQUEST))


@api_view(('PATCH',))
@authentication_classes([JWTAuthentication])
@permission_classes([IsAdminUser | IsProvider])
@transaction.atomic
def location_image_change_api(request, location_pk):
    try:
        if not request.data.get('image'):
            return Response({
                'success': False,
                'message': 'Image is required'
            }, status=status.HTTP_400_BAD_REQUEST)
        get_location = Location.objects.filter(id=location_pk).first()
        if not get_location:
            return Response({
                'success': False,
                'message': f'Failed to upload image because location does not exist'
            }, status=status.HTTP_400_BAD_REQUEST)
        get_image = request.FILES.getlist('image')[0]
        if get_location.image:
            delete_file(get_location.image.name)
        get_location.image = get_image
        get_location.save()
        return Response(
            {'id': get_location.id, 'updated_at': get_location.updated_at,
             'created_at': get_location.created_at, 'name': get_location.name,
             'image': get_location.image.url if get_location.image else None,
             'contact_person': get_location.contact_person,
             'operating_hours': OperatingHourLocation.objects.filter(location=get_location).all().values(),
             'email': get_location.email, 'address': get_location.address,
             'phone_number': get_location.phone_number, 'room_map': get_location.room_map.url if get_location.room_map else None},
            status=status.HTTP_200_OK
        )
    except Exception as e:
        return Response({
            'success': False,
            'message': f'Failed to upload image because of {str(e)}'
        }, status=status.HTTP_400_BAD_REQUEST)


@api_view(('GET', 'PATCH', 'DELETE'))
@authentication_classes([JWTAuthentication])
@permission_classes([IsAdminUser])
@transaction.atomic
def location_id_api(request, location_id):
    location = get_object_or_404(Location, id=location_id)
    if request.method == 'PATCH':

        data = request.data
        operating_hours = None
        closed_dates = None
        if 'operating_hours' in data:
            operating_hours = data.pop('operating_hours')
        if 'closed_dates' in data:
            closed_dates = data.pop('closed_dates')

        for field_name, field_value in data.items():
            if hasattr(Location, field_name):
                setattr(location, field_name, field_value)

        location.save()

        if operating_hours:
            OperatingHourLocation.objects.filter(location=location).delete()
            save_operating_hours(location, operating_hours)
        if closed_dates:
            LocationClosedDate.objects.filter(location=location).delete()
            save_closed_dates(location, closed_dates)

        return Response({"msg": "Location has been updated"}, status=status.HTTP_200_OK)

    if request.method == 'GET':
        return Response(
            {'id': location.id, 'updated_at': location.updated_at,
             'created_at': location.created_at, 'name': location.name,
             'image': location.image.url if location.image else None,
             'room_map': location.room_map.url if location.room_map else None,
             'room_map_mapping': location.room_map_mapping,
             'contact_person': location.contact_person,
             'operating_hours': OperatingHourLocation.objects.filter(location=location).all().values(),
             'email': location.email, 'address': location.address,
             'closed_dates': list(location.closed_dates.values()),
             'phone_number': location.phone_number},
            status=status.HTTP_200_OK
        )

    if request.method == 'DELETE':
        if location.image:
            delete_location_image = delete_file(location.image.url)
            if delete_location_image[0]:
                location.delete()
            else:
                return Response({
                    'success': False,
                    'message': f'Failed to delete location because of {delete_location_image[1]}'
                })
        else:
            location.delete()
        return Response({"msg": "Location has been deleted"}, status=status.HTTP_204_NO_CONTENT)


@api_view(('GET', 'POST'))
@authentication_classes([JWTAuthentication])
@permission_classes([IsAdminUser])
@transaction.atomic
def location_tier_setting_api(request, location_id):
    location = Location.objects.filter(id=location_id).first()
    if location:
        if request.method == 'POST':
            data = request.data.get('tier_setting', None)
            for query in data:
                query['location'] = location
                location_tier = LocationTierSetting.objects.create(**query)

            return Response({"msg": 'Location has been added'}, status=status.HTTP_201_CREATED)

        if request.method == 'GET':
            ordering = request.GET.get('ordering', 'tier')
            location_tier_setting = LocationTierSetting.objects.filter(location=location).order_by(ordering)
            locations_tiers = list()
            for query in location_tier_setting:
                location = model_to_dict(query,
                                         fields=['id', 'tier', 'start_hours', 'end_hours'])
                locations_tiers.append(location)
            return paginated_queryset_response(queryset=locations_tiers, request=request)
    else:
        return Response({"msg": "Location Does Not exist"}, status=status.HTTP_400_BAD_REQUEST)


@api_view(('DELETE', 'PATCH'))
@authentication_classes([JWTAuthentication])
@permission_classes([IsAdminUser])
@transaction.atomic
def location_tier_setting_id_api(request, location_id, tier_id):
    tier = LocationTierSetting.objects.filter(location__id=location_id, id=tier_id).first()
    if tier:
        if request.method == 'DELETE':
            tier.delete()
            return Response({"msg": "Location's Tier Setting has been deleted"}, status=status.HTTP_204_NO_CONTENT)

        if request.method == 'PATCH':
            data = request.data
            for field_name, field_value in data.items():
                if hasattr(LocationTierSetting, field_name):
                    setattr(tier, field_name, field_value)
            tier.save()
            return Response({"msg": "Location's Tier Setting has been updated"}, status=status.HTTP_200_OK)
    else:
        return Response({"msg": "Tier does not exist"}, status=status.HTTP_404_NOT_FOUND)


@api_view(('POST', 'GET'))
@authentication_classes([JWTAuthentication])
@permission_classes([IsAdminUser | IsProvider])
@transaction.atomic
def lock_api(request, location_id=None):
    if request.method == 'POST':
        location = get_object_or_404(Location, id=location_id)
        data = request.data
        data['location'] = location
        Lock.objects.create(**data)
        return Response({"msg": "Lock has been created"}, status=status.HTTP_201_CREATED)

    if request.method == 'GET':
        try:
            with schema_context("public"):
                ordering = request.GET.get('ordering', '-created_at')
                query = Lock.objects.all().order_by(ordering)
                locks = list()
                for lock in query:
                    lock = model_to_dict(lock,
                              exclude=['created_at', 'updated_at'])
                    locks.append(lock)
                return paginated_queryset_response(locks, request)
        except Exception as e:
            return Response({'msg': 'Something went wrong'}, status=status.HTTP_400_BAD_REQUEST)
@api_view(('DELETE', 'GET', 'PATCH'))
@authentication_classes([JWTAuthentication])
@permission_classes([IsAdminUser])
@transaction.atomic
def lock_id_api(request, location_id, lock_id):
    lock = Lock.objects.filter(id=lock_id, location__id=location_id).first()
    if lock:
        if request.method == 'DELETE':
            lock.delete()
            return Response({"msg": "Lock has been deleted"}, status=status.HTTP_204_NO_CONTENT)

        if request.method == 'PATCH':
            data = request.data
            for field_name, field_value in data.items():
                if hasattr(Lock, field_name):
                    setattr(lock, field_name, field_value)
            lock.save()

            return Response({"msg": "Lock has been updated"}, status=status.HTTP_200_OK)

        if request.method == 'GET':
            return Response({'name': lock.name, 'id': lock.id, 'token': lock.token, 'location': lock.location.id})
    else:
        return Response({"msg": "Lock Does Not Exist"}, status=status.HTTP_400_BAD_REQUEST)


@api_view(('POST',))
@authentication_classes([JWTAuthentication])
@permission_classes([IsAdminUser])
@transaction.atomic
def test_api(request, location_id=None):
    token = request.data["token"]
    try:
        response = request_unlock(token)
        if response["ok"]:
            return Response(None, status=status.HTTP_204_NO_CONTENT)
        else:
            return Response({'msg': 'Something went wrong'}, status=status.HTTP_502_BAD_GATEWAY)
    except Exception as e:
        traceback.print_exc()
        return Response({"message": "Something went wrong with the unlock command"},
                        status.HTTP_500_INTERNAL_SERVER_ERROR)


@api_view(('POST',))
@authentication_classes([JWTAuthentication])
@permission_classes([IsAdminUser | IsProvider])
@transaction.atomic
def unlock(request, location_id=None, lock_id=None):
    with schema_context("public"):
        lock = Lock.objects.filter(location__id=location_id).get(id=lock_id)
        try:
            response = request_unlock(lock.token)
            if response["ok"]:
                return Response(None, status=status.HTTP_204_NO_CONTENT)
            else:
                return Response(response, status=status.HTTP_502_BAD_GATEWAY)
        except Exception as e:
            traceback.print_exc()
            return Response({"message": "Something went wrong with the unlock command"},
                        status.HTTP_500_INTERNAL_SERVER_ERROR)


@api_view(('POST', 'GET'))
@authentication_classes([JWTAuthentication])
@permission_classes([IsAdminUser | IsWelcomeBoardAccount])
@transaction.atomic
def room_api(request, location_id=None):
    location = get_object_or_404(Location, id=location_id)
    if request.method == 'POST':
        data = request.data
        data['location'] = location
        operating_hours = data.pop('operating_hours', None)
        closed_dates = data.pop('closed_dates', None)
        room_authorization = data.pop('room_authorization', None)
        tiers_excluded = data.get('tiers_excluded')
        if tiers_excluded is False:
            room_prices = data.pop('room_prices', None)
        room = Room.objects.create(**data)

        if operating_hours:
            save_room_operating_hours(room, operating_hours)

        if closed_dates:
            save_room_closed_dates(room, closed_dates)

        if room_prices:
            save_room_price(room, room_prices)

        if room_authorization:
            save_room_authorization(room, room_authorization)

        return Response({"msg": "Room has been created"}, status=status.HTTP_201_CREATED)

    if request.method == 'GET':
        ordering = request.GET.get('ordering', 'order')
        queryset = Room.objects.all().order_by(ordering)
        if location:
            queryset = queryset.filter(location=location)
        rooms = list()
        for q in queryset:
            room = model_to_dict(q,
                                 exclude=['comment', 'tiers_excluded', 'order'])
            rooms.append(room)

        return Response({'result': rooms, 'count': len(rooms)})


@api_view(('GET', 'PATCH', 'DELETE'))
@authentication_classes([JWTAuthentication])
@permission_classes([IsAdminUser])
@transaction.atomic
def room_id_api(request, location_id=None, room_id=None):
    room = Room.objects.filter(location__id=location_id).get(id=room_id)
    if room:
        if request.method == 'PATCH':
            data = request.data
            operating_hours = data.pop('operating_hours', None)
            closed_dates = data.pop('closed_dates', None)
            room_authorization = data.pop('room_authorization', None)
            room_prices = data.pop('room_prices', None)

            for field_name, field_value in data.items():
                if hasattr(Room, field_name):
                    setattr(room, field_name, field_value)

            room.save()

            if operating_hours:
                RoomOperatingHours.objects.filter(room=room).delete()
                save_room_operating_hours(room, operating_hours)

            if closed_dates:
                RoomClosedDate.objects.filter(room=room).delete()
                save_room_closed_dates(room, closed_dates)

            if room_prices:
                RoomPrice.objects.filter(room=room).delete()
                save_room_price(room, room_prices)

            if room_authorization:
                RoomAuthorization.objects.filter(room=room).delete()
                save_room_authorization(room, room_authorization)

            return Response({"msg": "Room has been updated"}, status=status.HTTP_201_CREATED)

        if request.method == 'GET':
            operating_hours = room.operating_hours.values()
            closed_date = room.closed_date.values()
            room_prices = room.room_prices.values()
            room_authorization = room.room_authorization.all()

            room_authorization_data = []
            for authorization in room_authorization:
                room_authorization_data.append({
                    'id': authorization.id,
                    'room': authorization.room.id,
                    'permission': int(authorization.permission),
                    'user': {
                        'id': authorization.user.id,
                        'first_name': authorization.user.first_name,
                        'last_name': authorization.user.last_name,
                        'middle_name': authorization.user.middle_name
                    }
                })

            room = model_to_dict(room, exclude=['created_at', 'updated_at', ' created_by', 'updated_by'])

            room['operating_hours'] = operating_hours
            room['closed_date'] = list()
            for date in closed_date:
                room['closed_date'].append({
                    'id': date['id'],
                    'room_id': date['room_id'],
                    'date': str(int(date['date'].timestamp()))
                })
            room['room_prices'] = room_prices
            room['room_authorization'] = room_authorization_data

            return Response(room, status=status.HTTP_200_OK)

        if request.method == 'DELETE':
            room_id = room.id
            room.delete()

            thread = Thread(target=delete_bookings_for_room, args=(room_id,))
            thread.start()

            return Response({"msg": "Room has been deleted"}, status=status.HTTP_204_NO_CONTENT)
    else:
        return Response({"msg": "Room does not exist"}, status=status.HTTP_400_BAD_REQUEST)


@api_view(('GET', 'PATCH', 'DELETE'))
@authentication_classes([JWTAuthentication])
@permission_classes([IsAdminUser])
@transaction.atomic
def room_order_update(request):
    rooms = request.data.get('rooms')
    for room in rooms:
        Room.objects.filter(id=room['id']).update(order=room['order'])
    return Response('Orders have been updated')


# @api_view(('GET', 'PATCH', 'DELETE'))
# @authentication_classes([JWTAuthentication])
# @permission_classes([IsAdminUser | IsProvider])
# def get_valid_rooms(self, request, *args, location_pk=None, **kwargs):
#     if location_pk is None:
#         return Response("Location is required")
#
#     data = request.data.copy()
#     booking = Booking.objects.get(id=request.data.get('booking'))
#
#     data['location'] = location_pk
#     data['start_time'] = request.data.get('start_time', booking.start_time)
#     data['end_time'] = request.data.get('end_time', booking.end_time)
#     data['start_date'] = request.data.get('start_date', None)
#     data['provider'] = request.data.get('provider')
#
#     start_date = None
#
#     if data['start_date'] is None:
#         start_date = booking.start_date
#     else:
#         start_date = datetime.fromtimestamp(int(data.get('start_date')))
#
#     serializer = ValidRoomsSerializer(data=data)
#     serializer.is_valid(raise_exception=True)
#
#     end_time = serializer.validated_data.get('end_time')
#     start_time = serializer.validated_data.get('start_time')
#     location = serializer.validated_data.get('location')
#     provider = serializer.validated_data.get('provider')
#
#     week_date = parse_date(str(start_date.date()))
#     week_day = week_date.isoweekday()
#
#     valid_location_blackout_dates = LocationClosedDate.objects. \
#         filter(Q(location=location) &
#                Q(date__date=start_date.date())).count()
#     if valid_location_blackout_dates > 0:
#         raise ValidationError({"msg": "Location is in the closed date"})
#
#     queryset = Room.objects.filter(location=location)
#
#     queryset = queryset.prefetch_related('closed_date',
#                                          'booking_room',
#                                          'room_authorization',
#                                          'operating_hours')
#
#     bookings = Booking.objects.filter(Q(start_date__date=start_date.date()),
#                                       Q(location=location),
#                                       (
#                                               Q(start_time__range=(start_time, end_time)) |
#                                               Q(end_time__range=(start_time, end_time)) |
#                                               (Q(start_time__lte=start_time) & Q(
#                                                   end_time__gte=end_time))
#                                       )
#                                       ).exclude(status=BookingStatusChoices.CANCELED.value)
#
#     queryset = queryset.exclude(booking_room__id__in=bookings)
#
#     queryset = queryset.exclude(closed_date__date__date=start_date.date())
#     queryset = queryset.filter(Q(Q(room_authorization__user=provider) &
#                                  Q(room_authorization__permission=RoomPermission.AUTHORIZED.value)) | Q(
#         room_authorization__isnull=True))
#
#     queryset = queryset.exclude(
#         Q(operating_hours__start_time__gte=start_time) &
#         Q(operating_hours__close_time__lte=end_time))
#     queryset = queryset.filter(Q(operating_hours__is_useable=True) & Q(operating_hours__day_of_week=week_day))
#     response_serializer = RoomListSerializer(queryset, many=True)
#
#     paginated_data = self.paginate_queryset(response_serializer.data)
#
#     return self.get_paginated_response(paginated_data)


@api_view(('GET',))
@authentication_classes([JWTAuthentication])
@permission_classes([IsAdminUser])
@transaction.atomic
def admin_booking_detail(request):
    if request.method == 'GET':
        ordering = request.GET.get('ordering', '-start_date')
        querysets = AdminBookingDetails.objects.all().order_by(ordering)
        querysets = querysets.prefetch_related(
            'provider',
            'location',
            'room',
        )
        querysets = querysets.annotate(duration=F('end_time') - F('start_time'))

        querysets = booking_filter_queryset(queryset=querysets, request=request)
        querysets = date_range_filter_queryset(queryset=querysets, request=request)

        offset = int(request.GET.get('offset', 0))
        limit = int(request.GET.get('limit', 10))
        count = querysets.count()
        querysets = querysets[offset: offset + limit]

        response_data = []
        for queryset in querysets:

            provider = queryset.provider
            location = queryset.location
            room = queryset.room
            data = {
                'id': queryset.id,
                'provider__id': '',
                'provider__first_name': '',
                'provider__middle_name': '',
                'provider__last_name': '',
                'provider__avatar': '',
                'provider__status': '',
                'start_date__date': queryset.start_date.date(),
                'start_time': queryset.start_time,
                'created_at': queryset.created_at if queryset.created_at else None,
                'end_time': queryset.end_time,
                'duration': queryset.duration if queryset.duration else None,
                'status': queryset.status,
                'room_cost': queryset.room_cost,
                'room__id': '',
                'location__id': '',
                'location__name': '',
                'room__name': '',
                'appointments_start_time': queryset.appointments_start_time if queryset.appointments_start_time else None,
                'appointments_end_time': queryset.appointments_end_time if queryset.appointments_end_time else None
            }

            if provider:
                data['provider__id'] = provider.id
                data['provider__last_name'] = provider.last_name if provider.last_name else ''
                data['provider__avatar'] = provider.avatar.url if provider.avatar else ''
                data['provider__first_name'] = provider.first_name if provider.first_name else ''
                data['provider__middle_name'] = provider.middle_name if provider.middle_name else ''
                data['provider__status'] = provider.status

            if location:
                data['location__id'] = location.id
                data['location__name'] = location.name

            if room:
                data['room__id'] = room.id
                data['room__name'] = room.name

            response_data.append(data)

        return Response({'offset': offset, 'limit': limit, 'count': count, 'result': response_data}, status=status.HTTP_200_OK)


@api_view(('GET', 'PATCH', 'DELETE'))
@authentication_classes([JWTAuthentication])
@permission_classes([IsAdminUser])
@transaction.atomic
def admin_booking_detail_id_api(request, booking_id):
    if request.method == 'GET':
        queryset = AdminBookingDetails.objects.filter(id=booking_id).first()
        if queryset:
            # queryset = queryset.prefetch_related(
            #     'provider',
            #     'location',
            #     'room',
            #     'appointments'
            # )
            # queryset = queryset.annotate(duration=F('end_time') - F('start_time'))

            response_date = {
                'id': queryset.id,
                'start_date': queryset.start_date.timestamp(),
                'start_time': queryset.start_time,
                'end_time': queryset.end_time,
                'location': {
                    "id": queryset.location.id,
                    'name': queryset.location.name
                },
                'room': {
                    'id': queryset.room.id,
                    'name': queryset.room.name,
                },
                'room_cost': queryset.room_cost,
                'provider': {
                    "id": queryset.provider.id,
                    "first_name": queryset.provider.first_name,
                    "last_name": queryset.provider.last_name,
                    "middle_name": queryset.provider.middle_name,
                    "avatar": queryset.provider.avatar.url if queryset.provider.avatar else None
                }

            }

            return Response(response_date)
        else:
            raise ValidationError('Booking Does not exist')

    if request.method == 'PATCH':
        data = request.data
        queryset = AdminBookingDetails.objects.filter(id=booking_id).first()
        if 'status' in data:
            status = data['status']
            if status and status == str(BookingStatusChoices.CANCELED.value):
                queryset.status = BookingStatusChoices.CANCELED.value
                queryset.save()
                provider = queryset.provider
                with schema_context(provider.schema_name):
                    booking = Booking.objects.filter(id=booking_id).first()
                    if booking:
                        cancel_booking(provider, booking)

            return Response({"msg": "Booking Canceled"}, status=200)

        if 'location' in data:
            location = Location.objects.filter(id=data['location']).first()
            data['location'] = location
        if 'room' in data:
            room = Room.objects.filter(id=data['room']).first()
            data['room'] = room
        if 'start_date' in data:
            start_date = datetime.fromtimestamp(data['start_date'])
            data['start_date'] = start_date
        queryset.location = data.get('location', queryset.location)
        queryset.room = data.get('room', queryset.room)
        queryset.start_date = data.get('start_date', queryset.start_date)
        queryset.start_time = data.get('start_time', queryset.start_time)
        queryset.end_time = data.get('end_time', queryset.end_time)
        queryset.save()
        provider = queryset.provider
        with schema_context(provider.schema_name):
            booking = Booking.objects.filter(id=queryset.id).first()
            data['start_date'] = data['start_date'].timestamp()
            data = validate_update_booking_api(booking, request.data)
            for field_name, field_value in data.items():
                if hasattr(Booking, field_name):
                    if field_name == 'room':
                        setattr(booking, field_name, field_value.id)
                    else:
                        setattr(booking, field_name, field_value)

            booking.save()

            appointment = Appointment.objects.filter(booking=booking).first()

            if appointment:
                appointment.start_date = booking.start_date
                booking_start_time = datetime.strptime(booking.start_time, '%H:%M:%S').time()
                booking_end_time = datetime.strptime(booking.end_time, '%H:%M:%S').time()
                appointment_start_time = appointment.start_time
                appointment_end_time = appointment.end_time

                # Create tuples for booking_time_range and appointment_time_range
                booking_time_range = (booking_start_time, booking_end_time)
                appointment_time_range = (appointment_start_time, appointment_end_time)

                if appointment_time_range[1] < booking_time_range[0] or appointment_time_range[0] > booking_time_range[
                    1]:
                    appointment.start_time = booking.start_time
                    appointment.end_time = booking.end_time

                appointment.save()

        return Response({'msg': 'Booking updated'}, status=200)

    if request.method == 'DELETE':
        queryset = AdminBookingDetails.objects.filter(id=booking_id).first()
        if queryset:
            provider = queryset.provider
            queryset.delete()

            with schema_context(provider.schema_name):
                Booking.objects.filter(id=booking_id).delete()

        return Response("Booking Deleted", status=204)


@api_view(('GET',))
@authentication_classes([JWTAuthentication])
@permission_classes([IsProvider])
def calendar_location_api(request):
    if request.method == 'GET':
        locations = Location.objects.all()
        location_data = []
        for location in locations:
            location_data.append({'id': location.id, 'name': location.name,
                                  'room_map': location.room_map.url if location.room_map else None,
                                  'room_map_mapping': location.room_map_mapping if location.room_map_mapping else None})
        return paginated_queryset_response(location_data, request)


@api_view(('POST',))
@authentication_classes([JWTAuthentication])
@permission_classes([IsProvider])
@transaction.atomic
def valid_rooms_select_dates(request, location_pk=None):
    if location_pk is None:
        return Response({'error': 'location_pk is required'}, status=400)
    with schema_context("public"):
        location = Location.objects.get(pk=location_pk)
        provider = request.data.get('provider', request.user.id)
        request_data = request.data.copy()
        request_data['location'] = location_pk

        if 'provider' not in request.data:
            request_data['provider'] = request.user.id

        selected_dates = request_data.get('selected_dates', [])
        start_time = request_data.get('start_time')
        end_time = request_data.get('end_time')

        if not all([selected_dates, start_time, end_time]):
            return Response({'error': 'selected_dates, start_time, and end_time are required'}, status=400)

        selected_dates = [datetime.fromtimestamp(date).date() for date in selected_dates]

        valid_location_blackout_dates = LocationClosedDate.objects.filter(
            location=location,
            date__date__in=selected_dates
        ).count()
        if valid_location_blackout_dates > 0:
            return Response({"error": "Location is in the closed date"}, status=400)

        start_datetime = datetime.strptime(start_time, '%H:%M:%S').time()
        close_datetime = datetime.strptime(end_time, '%H:%M:%S').time()
        day_of_week = [date.isoweekday() for date in selected_dates]

        operating_hours = OperatingHourLocation.objects.filter(location=location, day_of_week__in = day_of_week, is_useable = True)
        for hour in operating_hours:
            if hour.start_time <= start_datetime and hour.close_time >= close_datetime:
                break
        else:
            return Response(status=status.HTTP_200_OK)

        rooms = Room.objects.filter(location=location)

        bookings = AdminBookingDetails.objects.filter(
            start_date__date__in=selected_dates,
            location=location,
            start_time__lt=end_time,
            end_time__gt=start_time,
        ).exclude(Q(end_time=start_time)).exclude(status=BookingStatusChoices.CANCELED.value)

        rooms = rooms.exclude(booking_room__in=bookings)

        rooms = rooms.exclude(
            closed_date__date__date__in=selected_dates
        ).filter(
            Q(room_authorization__user=provider, room_authorization__permission=RoomPermission.AUTHORIZED.value) |
            Q(room_authorization__isnull=True)
        )


        for room in rooms:
            operating_hours = RoomOperatingHours.objects.filter(room=room, day_of_week__in=day_of_week, is_useable=True)
            for hour in operating_hours:
                if hour.start_time <= start_datetime and  hour.close_time >= close_datetime:
                    break
            else:
                rooms = rooms.exclude(id=room.id)


        rooms = rooms.distinct()

        ordering = request.query_params.get('ordering')
        if ordering:
            rooms = rooms.order_by(ordering)

        rooms_data = []
        for room in rooms:
            rooms_data.append(model_to_dict(room))

        return Response({'result': rooms_data})


@api_view(['POST', ])
@authentication_classes([JWTAuthentication])
@permission_classes([IsProvider | IsAdminUser])
@transaction.atomic
def get_valid_rooms(request, location_pk=None):
    if location_pk is None:
        raise ValueError('location_pk is required')

    data = request.data.copy()
    booking = AdminBookingDetails.objects.get(id=request.data.get('booking'))

    data['location'] = location_pk
    data['start_time'] = request.data.get('start_time', booking.start_time)
    data['end_time'] = request.data.get('end_time', booking.end_time)
    data['start_date'] = request.data.get('start_date', None)
    data['provider'] = request.data.get('provider')

    start_date = None

    if data['start_date'] is None:
        start_date = booking.start_date
    else:
        start_date = datetime.fromtimestamp(int(data.get('start_date')))

    end_time = data.get('end_time')
    start_time = data.get('start_time')
    location = data.get('location')
    provider = data.get('provider')

    week_date = parse_date(str(start_date.date()))
    week_day = week_date.isoweekday()

    valid_location_blackout_dates = LocationClosedDate.objects.filter(
        Q(location=location) & Q(date__date=start_date.date())
    ).count()
    if valid_location_blackout_dates > 0:
        raise ValueError("Location is in the closed date")

    queryset = Room.objects.filter(location=location)

    queryset = queryset.prefetch_related(
        'closed_date', 'booking_room', 'room_authorization', 'operating_hours'
    )

    bookings = AdminBookingDetails.objects.filter(
        Q(start_date__date=start_date.date()),
        Q(location=location),
        (
                Q(start_time__range=(start_time, end_time))
                | Q(end_time__range=(start_time, end_time))
                | (Q(start_time__lte=start_time) & Q(end_time__gte=end_time))
        ),
    ).exclude(status=BookingStatusChoices.CANCELED.value)

    queryset = queryset.exclude(booking_room__id__in=bookings)

    queryset = queryset.exclude(closed_date__date__date=start_date.date())
    queryset = queryset.filter(
        Q(Q(room_authorization__user=provider) & Q(room_authorization__permission=RoomPermission.AUTHORIZED.value))
        | Q(room_authorization__isnull=True)
    )

    queryset = queryset.exclude(
        Q(operating_hours__start_time__gte=start_time) & Q(operating_hours__close_time__lte=end_time)
    )
    queryset = queryset.filter(
        Q(operating_hours__is_useable=True) & Q(operating_hours__day_of_week=week_day)
    )

    # Serialize queryset directly
    response_data = []
    for room in queryset:
        room_data = {
            'id': room.id,
            'location': {
                'id': room.location.id,
                'name': room.location.name
            },
            'name': room.name,
            'tiers_excluded': room.tiers_excluded,
            'order': room.order,
            'comment': room.comment,
            'room_prices': [
                {
                    'id': price.id,
                    'room': price.room_id,
                    'tier': price.tier_id,
                    'cost': price.cost
                }
                for price in room.room_prices.all()
            ],
            'operating_hours': [
                {
                    'id': hours.id,
                    'room': hours.room_id,
                    'day_of_week': hours.day_of_week,
                    'start_time': hours.start_time,
                    'close_time': hours.close_time,
                    'is_useable': hours.is_useable
                }
                for hours in room.operating_hours.all()
            ],
            'closed_date': [
                {
                    'room': closed.room_id,
                    'date': closed.date
                }
                for closed in room.closed_date.all()
            ],
            'room_authorization': [
                {
                    'id': auth.id,
                    'room': auth.room_id,
                    'user': {
                        'id': auth.user.id,
                        'first_name': auth.user.first_name,
                        'middle_name': auth.user.middle_name,
                        'last_name': auth.user.last_name
                    },
                    'permission': auth.permission
                }
                for auth in room.room_authorization.all()
            ]
        }
        response_data.append(room_data)

    return paginated_queryset_response(response_data, request)


@api_view(('POST',))
@authentication_classes([JWTAuthentication])
@permission_classes([IsProvider])
@transaction.atomic
def my_tier(request):
    provider = request.user
    with schema_context("public"):
        start_of_week = datetime_helper.start_of_week()

        bookings = AdminBookingDetails.objects.filter(
            provider_id=provider.id,
            created_at__gte=start_of_week,
            room__isnull=False,
        )

        location = request.data['location']
        bookings = bookings.annotate(
            duration=(F('end_time') - F('start_time')))

        duration = bookings.aggregate(Sum('duration')).get('duration__sum')
        total_hours = duration.total_seconds() / 3600 if \
            duration is not None else 0

        # tiers = get_tiers(provider, request.data['location'], total_hours)
        # tiers = LocationTierSetting.objects.filter(location=location)
        # my_tier = None
        # for tier in tiers:
        #     range = [tier.start_hours, tier.end_hours]
        #
        #     if range[1] >= total_hours:
        #         my_tier = tier
        #
        #     if range[0] <= total_hours <= range[1]:
        #         my_tier = tier
        #         break  # Conditions does not satisfy if total_room_hours are in decimal value

        tiers = LocationTierSetting.objects.filter(location=location).order_by('-start_hours')
        my_tier = None
        for tier in tiers:
            range = [tier.start_hours, tier.end_hours]

            if range[0] <= total_hours <= range[1]:
                my_tier = tier
                break

        # If my_tier is still None, meaning total_hours didn't fit into any tier
        if my_tier is None and tiers:
            my_tier = tiers[0]  #

        cost = 0
        if my_tier:
            room = request.data['room']
            room = Room.objects.filter(id=room).first()
            if room.tiers_excluded:
                cost = 0
            else:
                room_prices = RoomPrice.objects.filter(room=room)
                for room_price in room_prices:
                    if room_price.tier.id == tier.id:
                        cost = room_price.cost

        return Response({'room_cost': cost})


@api_view(['GET'])
@authentication_classes([JWTAuthentication])
@permission_classes([IsAdminUser])
def provider_billing_api(request):
    if request.method == 'GET':
        offset = int(request.GET.get('offset', 0))
        limit = int(request.GET.get('limit', 10))
        ordering = request.GET.get('ordering', '-created_at')
        invoices = ProviderBillings.objects.all().order_by(ordering)

        search = request.GET.get('search', None)
        if search:
            invoices = invoices.filter(
            Q(provider__first_name__icontains=search) |
            Q(provider__last_name__icontains=search) |
            Q(provider__middle_name__icontains=search)
        )

        status = request.GET.get('status', None)
        if status:
            invoices = invoices.filter(status=status)

        provider_type = request.GET.get('provider_type', None)
        if provider_type:
            provider_type = eval(provider_type)
            invoices = invoices.filter(provider__provider_license__license_type__in=provider_type).distinct()
        count = invoices.count()
        invoices = invoices[offset: offset + limit]
        invoices_data = []
        for invoice in invoices:
            provider = invoice.provider
            provider_licenses = provider.provider_license.all()
            invoice_data = {
                'id': invoice.id,
                'invoice_number': invoice.invoice_number,
                'due_date': str(int(invoice.due_date.timestamp())) if invoice.due_date else None,
                'created_at': invoice.created_at.timestamp() if invoice.created_at else None,
                'status': invoice.status,
                'total_amount': invoice.total_amount,
                'provider': {
                    'id': provider.id,
                    'first_name': provider.first_name,
                    'middle_name': provider.middle_name,
                    'last_name': provider.last_name
                }
            }

            license_types = [{'license_type': license.license_type} for license in provider_licenses]
            invoice_data['provider']['provider_license'] = license_types

            # Append the invoice data to invoices_data
            invoices_data.append(invoice_data)

        return Response({'offset': offset, 'limit': limit, 'count': count, 'result': invoices_data})


@api_view(['GET', 'PATCH'])
@authentication_classes([JWTAuthentication])
@permission_classes([IsAdminUser])
def provider_billing_id_api(request, invoice_id=None):
    if request.method == 'GET':
        try:
            invoices = ProviderBillings.objects.filter(invoice_number=invoice_id).first()
            if invoices:
                provider = invoices.provider
                with schema_context(provider.schema_name):
                    invoice = Invoice.objects.filter(invoice_number=invoices.invoice_number).first()
                    bookings = invoice.bookings.all()
                    booking_data = []
                    for booking in bookings:
                        room = Room.objects.filter(id=booking.room).first()
                        prices = booking.prices.all()[0] if booking.prices.exists() else None

                        data = {'id': booking.id, 'start_time': booking.start_time, 'room_cost': booking.room_cost,
                                'booking_number': booking.booking_number,
                                'cancel_date': booking.cancel_date.timestamp() if booking.cancel_date else None,
                                'created_at': booking.created_at.timestamp() if booking.created_at else None,
                                'start_date': booking.start_date.timestamp(), 'status': booking.status,
                                'end_time': booking.end_time, 'room': {
                                'id': room.id,
                                'name': room.name,
                            }, 'location': {'id': room.location.id, 'name': room.location.name}}
                        if prices:
                            data['prices'] = {'id': prices.id, 'tier': prices.tier, 'hours': prices.hours,
                                              'price': prices.price}
                        booking_data.append(data)

                    created_at = invoice.created_at.timestamp() if invoice.created_at else None
                    invoice = model_to_dict(invoice)
                    invoice['bookings'] = booking_data
                    invoice['created_at'] = created_at
                    invoice['provider'] = {
                        'id': provider.id,
                        'first_name': provider.first_name,
                        'middle_name': provider.middle_name,
                        'last_name': provider.last_name
                    }

                    return Response(invoice)
            else:
                raise ValidationError({'msg': 'Invoice Error '})
        except Exception as e:
            print(e)
            raise ValidationError({'msg': 'Something went wrong'})
    if request.method == 'PATCH':
        try:
            status = request.data.get('status', None)
            if status is None:
                return Response({'msg': 'status is required'}, status=400)
            invoices = ProviderBillings.objects.filter(invoice_number=invoice_id).first()
            invoices.status = status
            invoices.save()
            if invoices:
                provider = invoices.provider
                with schema_context(provider.schema_name):
                    invoice = Invoice.objects.filter(invoice_number=invoices.invoice_number).first()
                    invoice.status=status
                    invoice.save()

                    return Response({"msg": "Invoice Updated"}, status=200)
            else:
                raise ValidationError({'msg': 'Invoice Error '})
        except Exception as e:
            print(e)
            raise ValidationError({'msg': 'Something went wrong'})

@api_view(['POST'])
@authentication_classes([JWTAuthentication])
@permission_classes([IsAdminUser])
def invoice_email_to_provider(request, pk=None):
        invoice = ProviderBillings.objects.filter(invoice_number=pk).first()
        if invoice:

            provider = invoice.provider
            from_email = settings.CONTACT_EMAIL
            to_email = provider.email

            if 'file' in request.data:
                file_b64 = request.data['file']
                file_b64 = file_b64.split(',')[-1]
                buffer = BytesIO()
                content = base64.b64decode(file_b64)
                buffer.write(content)

                html_template = get_template('provider_invoice_email_template.html')
                context = {
                    'full_name_provider': provider.first_name + ' ' + provider.last_name,
                }
                html_content = html_template.render(context)

                msg = EmailMultiAlternatives(
                    subject="Ethera - Invoice", from_email=from_email, to=[to_email])
                msg.attach_alternative(html_content, "text/html")
                msg.attach(f"invoice_{invoice.invoice_number}.pdf", buffer.getvalue(),
                           guess_type(f"invoice_{invoice.invoice_number}.pdf")[0])


                try:
                    msg.send()
                    return Response({"response": "Email Sent"}, status=status.HTTP_200_OK)
                except Exception as ex:
                    print(ex)
                    return Response({"response": 'Failed to send email'}, status=status.HTTP_400_BAD_REQUEST)
            else:
                return Response({"error": "Please attach file."}, status=status.HTTP_400_BAD_REQUEST)
        else:
            return Response({"msg": "Invoice not found"}, status=status.HTTP_400_BAD_REQUEST)


@api_view(['GET'])
@authentication_classes([])
@permission_classes([])
def kiosk_location_api(request):
    queryset = Location.objects.all().values('id', 'name')
    if queryset:
        return paginated_queryset_response(queryset, request)
    else:
        return Response({
            'msg': 'There is no location'
        }, status=status.HTTP_204_NO_CONTENT)


@api_view(['GET', 'POST'])
@authentication_classes([])
@permission_classes([])
def kiosk_provider_api(request, location_pk=None):
    try:
        if request.method == 'GET':
            if location_pk is None:
                return Response({'msg': 'Location is required'}, status=status.HTTP_400_BAD_REQUEST)

            providers = Provider.objects.filter(booking__location=location_pk,
                                                booking__start_date__date=timezone.now().date()).distinct()
            response_provider = []
            for provider in providers:
                data = {
                    'id': provider.id,
                    'first_name': provider.first_name if provider.first_name else '',
                    'middle_name': provider.middle_name if provider.middle_name else '',
                    'last_name': provider.last_name if provider.last_name else '',
                    'avatar': provider.avatar.url if provider.avatar else '',
                }
                for license in provider.provider_license.all():
                    data['provider_licenses'] = {
                        'license_type': license.license_type,
                        'license_number': license.license_number,
                        'other_license': license.other_license if license.other_license else '',
                        'expire_date': int(license.expire_date.timestamp()) if license.expire_date else None,
                        'state': license.state,
                    }
                response_provider.append(data)

            return Response({'count': providers.count(), 'result': response_provider})
        if request.method == 'POST':
            print(request.data['id'])

            instance = Provider.objects.filter(id=request.data['id']).first()
            print(instance)
            if instance is None:
                return Response({'msg': 'Provider is required'}, status=status.HTTP_400_BAD_REQUEST)
            client_initials = request.data.get('client_initials', None)
            if client_initials:
                kiosk_check_in.send(sender=instance, instance=instance, initials=client_initials)
            else:
                return Response({'msg': 'Client Initials are required'}, status=status.HTTP_400_BAD_REQUEST)
            return Response(200)
    except Exception as e:
        print(e)
        return Response({'msg': 'Something went wrong'}, status=status.HTTP_400_BAD_REQUEST)

@api_view(['GET'])
@authentication_classes([])
@permission_classes([])
def get_name_board_api(request, room_id=None):
    try:
        if room_id is None:
            return Response({'msg': 'Room is required'}, status=status.HTTP_400_BAD_REQUEST)

        date = request.GET.get('date', None)
        time = request.GET.get('time', None)

        if date is None or time is None:
            return Response({
                'msg': 'Date and time is required'
            }, status=status.HTTP_400_BAD_REQUEST)

        booking = AdminBookingDetails.objects.filter(start_date__date=date, start_time__lte=time,
                                                     end_time__gt=time, room__id=room_id).first()
        if booking:
            provider = booking.provider
            return Response({'first_name': provider.first_name, 'last_name': provider.last_name, 'practice': provider.practice}, status=status.HTTP_200_OK)
        else:
            return Response({'msg': 'Room is free'}, status=status.HTTP_200_OK)

    except Exception as e:
        return Response({'msg': 'Room is required'}, status=status.HTTP_400_BAD_REQUEST)